Initial implementation of simradio-based testing with a multi-node simulated mesh#946
Initial implementation of simradio-based testing with a multi-node simulated mesh#946ianmcorvidae wants to merge 8 commits into
Conversation
📝 WalkthroughWalkthroughThis PR adds a meshtasticd-based simulator test harness, shared helpers, fixtures, smoke tests, and CI/pytest wiring. It also changes ChangesFirmware Simulator Test Infrastructure
Estimated code review effort: 4 (Complex) | ~75 minutes Node.deleteChannel Write Optimization
Estimated code review effort: 2 (Simple) | ~12 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubActions
participant pytest
participant meshtasticd
GitHubActions->>pytest: run smokevirt or smokemesh
pytest->>meshtasticd: start simulator-based tests
pytest-->>GitHubActions: report results
Related Issues: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #946 +/- ##
==========================================
+ Coverage 66.84% 67.38% +0.53%
==========================================
Files 25 25
Lines 4741 4752 +11
==========================================
+ Hits 3169 3202 +33
+ Misses 1572 1550 -22
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
meshtastic/tests/test_smokevirt.py (2)
244-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUnused helper
_set_and_verify.This wrapper around
cli_then_verifyis defined but not called anywhere in the file; all tests callcli_then_verifydirectly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@meshtastic/tests/test_smokevirt.py` around lines 244 - 246, The helper _set_and_verify is unused and duplicates cli_then_verify, so remove the dead wrapper from the test file and update any remaining references if needed to call cli_then_verify directly. If you want to keep it for future use, add a real caller; otherwise delete the _set_and_verify function to avoid stale, misleading test helpers.
542-559: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor: prefer unpacking over list concatenation.
Ruff flags
["--pos-fields"] + _POS_FIELDS_INPUT(line 558);[*_POS_FIELDS_INPUT]-style unpacking is the idiomatic form.Suggested fix
cli_then_verify( firmware_node.port, - ["--pos-fields"] + _POS_FIELDS_INPUT, + ["--pos-fields", *_POS_FIELDS_INPUT], check, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@meshtastic/tests/test_smokevirt.py` around lines 542 - 559, The smoke test in test_smokevirt_pos_fields builds the CLI arguments with list concatenation, which Ruff prefers to be unpacked idiomatically. Update the cli_then_verify call in test_smokevirt_pos_fields to pass the --pos-fields arguments using unpacking with _POS_FIELDS_INPUT instead of ["--pos-fields"] + _POS_FIELDS_INPUT, keeping the rest of the test logic unchanged.Source: Linters/SAST tools
.github/workflows/ci.yml (1)
84-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated poetry setup vs the
validatejob.The
poetry install/version-check steps mirror thevalidatejob almost verbatim (differing only by--all-extras --with dev). Consider extracting a reusable composite action or workflow template if more jobs adopt this pattern, though it's not urgent for two jobs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 84 - 95, The Python/Poetry setup in the CI workflow duplicates the same install/version-check flow used by the validate job, so consolidate the shared setup behind a reusable workflow, composite action, or a single common step sequence. Update the job that runs the meshtastic install to reuse that shared logic and keep only the job-specific Poetry options such as all-extras and dev, using the existing validate job and its Poetry install steps as the reference points.meshtastic/tests/test_smokemesh.py (1)
94-99: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
test_smokemesh_show_nodesmakes no assertions.The test only invokes
showNodes()on each interface without validating output content, so it only verifies the call doesn't raise — it wouldn't catch missing/incorrect node entries.♻️ Suggested improvement using capsys
`@pytest.mark.smokemesh` -def test_smokemesh_show_nodes(firmware_mesh): +def test_smokemesh_show_nodes(firmware_mesh, capsys): """showNodes should report the other nodes in the mesh.""" for i in range(3): iface = firmware_mesh.get_iface(i) iface.showNodes() + out = capsys.readouterr().out + assert out.strip(), f"node {i} showNodes produced no output"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@meshtastic/tests/test_smokemesh.py` around lines 94 - 99, test_smokemesh_show_nodes only calls showNodes() and never verifies behavior, so add assertions to confirm the printed output contains the expected node information. Update the test to use capsys around firmware_mesh.get_iface(i).showNodes() and assert the captured output includes the other mesh nodes, so the test validates content rather than just absence of exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 84-89: The checkout step in the CI workflow is persisting
credentials by default, which should be disabled for this job. Update the
actions/checkout usage in the workflow to set persist-credentials to false,
keeping the token from being available after checkout while the job continues
with the PPA and meshtasticd installation steps.
In `@meshtastic/tests/conftest.py`:
- Around line 34-65: SimMesh startup can fail after some meshtasticd processes
are already launched, leaving orphaned nodes because SimMesh.stop() exits early
before _started is set. Update SimMesh.start() and/or the startup path in
firmware_harness.py to track partially started nodes and invoke cleanup on any
failure from connect() or later initialization steps. Use the SimMesh.start,
SimMesh.stop, and connect flow to ensure already-started processes are stopped
even when startup does not complete.
In `@meshtastic/tests/firmware_harness.py`:
- Around line 193-212: Partial startup failures in
`firmware_harness.FirmwareMesh.start` leave previously launched nodes running
because `_started` stays false and `stop()` won’t clean them up. Wrap the
`node.start()`/`node.connect()` setup in `start()` with failure handling that
stops any nodes already started and clears their resources before re-raising.
Use the existing `FirmwareMesh.start`, `FirmwareMesh.stop`, and node lifecycle
methods to ensure partial initialization is always torn down.
In `@meshtastic/tests/test_node.py`:
- Around line 729-742: The test in Node.deleteChannel’s primary-slot rejection
path uses an equality check for the exception type, which triggers Ruff E721.
Update the assertion in test_delete_channel_rejects_primary to compare
pytest_wrapped_e.type to SystemExit with identity semantics instead of equality,
keeping the rest of the SystemExit validation unchanged.
In `@meshtastic/tests/test_smokemesh.py`:
- Around line 102-128: The smokemesh traceroute test is using a fixed sleep and
an unguarded traceroute payload lookup. In
test_smokemesh_traceroute_across_relay, replace the time.sleep delay with
PacketCollector.wait_for(1) so the test waits deterministically for traceroute
packets, and update the a_resp decoded traceroute access to use a guarded
assertion-friendly lookup so missing payloads fail through the test assertion
rather than a KeyError.
In `@meshtastic/tests/test_smokevirt.py`:
- Around line 394-432: `test_smokevirt_seturl_default` currently bypasses the
`--seturl` CLI path by calling `iface.localNode.setURL` directly, so it does not
test argument parsing or CLI behavior. Update the test to use `run_cli` for the
URL mutation, then reconnect or use a fresh interface to verify the resulting
URL via `firmware_node`/`iface.localNode.getURL`, keeping the existing
payload-decoding assertions intact.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 84-95: The Python/Poetry setup in the CI workflow duplicates the
same install/version-check flow used by the validate job, so consolidate the
shared setup behind a reusable workflow, composite action, or a single common
step sequence. Update the job that runs the meshtastic install to reuse that
shared logic and keep only the job-specific Poetry options such as all-extras
and dev, using the existing validate job and its Poetry install steps as the
reference points.
In `@meshtastic/tests/test_smokemesh.py`:
- Around line 94-99: test_smokemesh_show_nodes only calls showNodes() and never
verifies behavior, so add assertions to confirm the printed output contains the
expected node information. Update the test to use capsys around
firmware_mesh.get_iface(i).showNodes() and assert the captured output includes
the other mesh nodes, so the test validates content rather than just absence of
exceptions.
In `@meshtastic/tests/test_smokevirt.py`:
- Around line 244-246: The helper _set_and_verify is unused and duplicates
cli_then_verify, so remove the dead wrapper from the test file and update any
remaining references if needed to call cli_then_verify directly. If you want to
keep it for future use, add a real caller; otherwise delete the _set_and_verify
function to avoid stale, misleading test helpers.
- Around line 542-559: The smoke test in test_smokevirt_pos_fields builds the
CLI arguments with list concatenation, which Ruff prefers to be unpacked
idiomatically. Update the cli_then_verify call in test_smokevirt_pos_fields to
pass the --pos-fields arguments using unpacking with _POS_FIELDS_INPUT instead
of ["--pos-fields"] + _POS_FIELDS_INPUT, keeping the rest of the test logic
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: db37b9e9-10fb-4ad6-88db-88ae9c7eee05
📒 Files selected for processing (9)
.github/workflows/ci.ymlmeshtastic/node.pymeshtastic/tests/conftest.pymeshtastic/tests/firmware_harness.pymeshtastic/tests/fw_helpers.pymeshtastic/tests/test_node.pymeshtastic/tests/test_smokemesh.pymeshtastic/tests/test_smokevirt.pypytest.ini
| @pytest.fixture(scope="function") | ||
| def firmware_node(): | ||
| """A single meshtasticd sim node for smokevirt tests. | ||
|
|
||
| Function-scoped so every test gets a freshly-erased node with no | ||
| state leaking from previous tests. This makes destructive commands | ||
| (``--reboot``, ``--set factory_reset true``) safe to run and lets | ||
| tests be order-independent. | ||
|
|
||
| Yields the SimNode instance. The node is booted with a fresh erased | ||
| config and listens on localhost at its TCP port. | ||
| """ | ||
| _skip_firmware_if_unavailable() | ||
| mesh = SimMesh(n_nodes=1, base_port=SINGLE_NODE_BASE_PORT) | ||
| mesh.start() | ||
| yield mesh.get_node(0) | ||
| mesh.stop() | ||
|
|
||
|
|
||
| @pytest.fixture(scope="function") | ||
| def firmware_mesh(): | ||
| """A 3-node chain (A-B-C) meshtasticd sim mesh for smokemesh tests. | ||
|
|
||
| Yields the SimMesh instance. Nodes are connected and the SIMULATOR_APP | ||
| packet bridge is running. Node DB convergence is awaited. | ||
| """ | ||
| _skip_firmware_if_unavailable() | ||
| mesh = SimMesh(n_nodes=3, topology=CHAIN_TOPOLOGY) | ||
| mesh.start() | ||
| mesh.wait_for_convergence(timeout=30) | ||
| yield mesh | ||
| mesh.stop() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect SimMesh.start/stop and SimNode.start/close for partial-failure cleanup handling
fd firmware_harness.py meshtastic/tests --exec cat -n {}Repository: meshtastic/python
Length of output: 14743
Handle partial startup cleanup in SimMesh.start()
mesh.start() can fail after some meshtasticd processes are already running, but SimMesh.stop() returns early until _started is set. Clean up already-started nodes inside firmware_harness.py so a failed connect() or later startup step doesn’t leave orphaned processes behind.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@meshtastic/tests/conftest.py` around lines 34 - 65, SimMesh startup can fail
after some meshtasticd processes are already launched, leaving orphaned nodes
because SimMesh.stop() exits early before _started is set. Update
SimMesh.start() and/or the startup path in firmware_harness.py to track
partially started nodes and invoke cleanup on any failure from connect() or
later initialization steps. Use the SimMesh.start, SimMesh.stop, and connect
flow to ensure already-started processes are stopped even when startup does not
complete.
| def start(self) -> None: | ||
| """Launch all nodes, connect, and start the packet bridge.""" | ||
| binary = find_meshtasticd() | ||
| if binary is None: | ||
| raise RuntimeError( | ||
| "meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH." | ||
| ) | ||
|
|
||
| for node in self.nodes: | ||
| node.start(binary) | ||
|
|
||
| for node in self.nodes: | ||
| node.connect() | ||
| self._port_to_idx[node.port] = node.node_id | ||
|
|
||
| pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator") | ||
| self._started = True | ||
|
|
||
| if self.n_nodes > 1: | ||
| self._trigger_convergence() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Partial startup failure leaks meshtasticd processes.
If node.start() (or node.connect()) raises after earlier nodes have already launched, the exception propagates with self._started still False. The firmware_mesh/firmware_node fixtures only call mesh.stop() after yield, and stop() early-returns when _started is False, so the already-launched simulator processes (and their temp dirs) are never cleaned up and become orphans that also hold onto the TCP ports.
Wrap startup so any already-started nodes are torn down before re-raising.
🔧 Proposed cleanup on failure
- for node in self.nodes:
- node.start(binary)
-
- for node in self.nodes:
- node.connect()
- self._port_to_idx[node.port] = node.node_id
-
- pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
- self._started = True
+ try:
+ for node in self.nodes:
+ node.start(binary)
+
+ for node in self.nodes:
+ node.connect()
+ self._port_to_idx[node.port] = node.node_id
+ except Exception:
+ for node in self.nodes:
+ node.close()
+ raise
+
+ pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
+ self._started = True📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def start(self) -> None: | |
| """Launch all nodes, connect, and start the packet bridge.""" | |
| binary = find_meshtasticd() | |
| if binary is None: | |
| raise RuntimeError( | |
| "meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH." | |
| ) | |
| for node in self.nodes: | |
| node.start(binary) | |
| for node in self.nodes: | |
| node.connect() | |
| self._port_to_idx[node.port] = node.node_id | |
| pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator") | |
| self._started = True | |
| if self.n_nodes > 1: | |
| self._trigger_convergence() | |
| def start(self) -> None: | |
| """Launch all nodes, connect, and start the packet bridge.""" | |
| binary = find_meshtasticd() | |
| if binary is None: | |
| raise RuntimeError( | |
| "meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH." | |
| ) | |
| try: | |
| for node in self.nodes: | |
| node.start(binary) | |
| for node in self.nodes: | |
| node.connect() | |
| self._port_to_idx[node.port] = node.node_id | |
| except Exception: | |
| for node in self.nodes: | |
| node.close() | |
| raise | |
| pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator") | |
| self._started = True | |
| if self.n_nodes > 1: | |
| self._trigger_convergence() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@meshtastic/tests/firmware_harness.py` around lines 193 - 212, Partial startup
failures in `firmware_harness.FirmwareMesh.start` leave previously launched
nodes running because `_started` stays false and `stop()` won’t clean them up.
Wrap the `node.start()`/`node.connect()` setup in `start()` with failure
handling that stops any nodes already started and clears their resources before
re-raising. Use the existing `FirmwareMesh.start`, `FirmwareMesh.stop`, and node
lifecycle methods to ensure partial initialization is always torn down.
| @pytest.mark.smokemesh | ||
| def test_smokemesh_traceroute_across_relay(firmware_mesh): | ||
| """Traceroute from A to C should show route via B in both directions.""" | ||
| col_a = subscribe_traceroutes(firmware_mesh.get_iface(0)) | ||
| col_c = subscribe_traceroutes(firmware_mesh.get_iface(2)) | ||
| try: | ||
| src_a = firmware_mesh.get_node(0).node_num | ||
| dest_c = firmware_mesh.get_node(2).node_num | ||
| node_b = firmware_mesh.get_node(1).node_num | ||
|
|
||
| firmware_mesh.get_iface(0).sendTraceRoute(dest=dest_c, hopLimit=3) | ||
|
|
||
| time.sleep(2) | ||
|
|
||
| assert len(col_a.traceroutes) >= 1, "A did not receive traceroute response" | ||
| a_resp = col_a.traceroutes[0] | ||
| assert a_resp["from"] == dest_c, "response source should be C" | ||
|
|
||
| route = a_resp["decoded"]["traceroute"] | ||
| assert route.get("route") == [node_b], "forward route should be A→B→C" | ||
| assert route.get("routeBack") == [node_b], "return route should be C→B→A" | ||
|
|
||
| assert len(col_c.traceroutes) >= 1, "C did not receive traceroute request" | ||
| c_req = col_c.traceroutes[0] | ||
| assert c_req["from"] == src_a, "request source should be A" | ||
| finally: | ||
| unsubscribe_all("meshtastic.receive.traceroute") |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect PacketCollector.wait_for signature and traceroute bridging in the harness
# to confirm whether wait_for works for traceroute topics and where the TRACEROUTE_APP
# packet bridging is implemented.
rg -n -A10 'def wait_for' meshtastic/tests/fw_helpers.py
rg -n -B2 -A15 'TRACEROUTE_APP|RouteDiscovery' meshtastic/tests/firmware_harness.pyRepository: meshtastic/python
Length of output: 639
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate traceroute collector helpers and related tests.
rg -n -A6 -B4 'subscribe_traceroutes|traceroutes|wait_for\(' meshtastic/tests
printf '\n--- firmware_harness traceroute handling ---\n'
rg -n -A8 -B4 'TRACEROUTE_APP|RouteDiscovery|traceroute' meshtastic/tests/firmware_harness.py
printf '\n--- test_smokemesh context ---\n'
sed -n '1,220p' meshtastic/tests/test_smokemesh.pyRepository: meshtastic/python
Length of output: 9159
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -A12 -B6 'def sendTraceRoute|waitForTraceRoute|send_trace_route' meshtastic/tests fw_helpers.py meshtastic/tests/firmware_harness.pyRepository: meshtastic/python
Length of output: 2363
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -A8 -B4 'decoded"\]\["traceroute"|get\("decoded", \{\}\)\.get\("traceroute"|decoded.*traceroute' meshtastic/testsRepository: meshtastic/python
Length of output: 1324
Replace the fixed sleep with wait_for(1) here. PacketCollector already supports deterministic polling for traceroute packets, and a_resp["decoded"]["traceroute"] should use a guarded lookup so a missing payload fails with the assertion instead of a KeyError.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@meshtastic/tests/test_smokemesh.py` around lines 102 - 128, The smokemesh
traceroute test is using a fixed sleep and an unguarded traceroute payload
lookup. In test_smokemesh_traceroute_across_relay, replace the time.sleep delay
with PacketCollector.wait_for(1) so the test waits deterministically for
traceroute packets, and update the a_resp decoded traceroute access to use a
guarded assertion-friendly lookup so missing payloads fail through the test
assertion rather than a KeyError.
Source: Pipeline failures
| @pytest.mark.smokevirt | ||
| def test_smokevirt_ensure_ch_del_second_of_three_channels(): | ||
| """Test that when we delete the 2nd of 3 channels, that it deletes the correct channel.""" | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-add testing1" | ||
| ) | ||
| assert re.match(r"Connected to radio", out) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info") | ||
| assert re.match(r"Connected to radio", out) | ||
| assert re.search(r"SECONDARY", out, re.MULTILINE) | ||
| assert re.search(r"testing1", out, re.MULTILINE) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-add testing2" | ||
| ) | ||
| assert re.match(r"Connected to radio", out) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info") | ||
| assert re.match(r"Connected to radio", out) | ||
| assert re.search(r"testing2", out, re.MULTILINE) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-del --ch-index 1" | ||
| ) | ||
| assert re.match(r"Connected to radio", out) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info") | ||
| assert re.match(r"Connected to radio", out) | ||
| assert re.search(r"testing2", out, re.MULTILINE) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-del --ch-index 1" | ||
| ) | ||
| assert re.match(r"Connected to radio", out) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| def test_smokevirt_seturl_default(firmware_node): | ||
| """--seturl applies a known channel URL.""" | ||
| url = "https://www.meshtastic.org/d/#CgUYAyIBAQ" | ||
|
|
||
| # Use the fixture's already-connected TCPInterface so the same | ||
| # connection handles the firmware restart after setURL. | ||
| if firmware_node.iface is None: | ||
| pytest.fail("fixture interface not connected") | ||
| iface = firmware_node.iface | ||
| iface.localNode.setURL(url) | ||
|
|
||
| @pytest.mark.smokevirt | ||
| def test_smokevirt_ensure_ch_del_third_of_three_channels(): | ||
| """Test that when we delete the 3rd of 3 channels, that it deletes the correct channel.""" | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-add testing1" | ||
| ) | ||
| assert re.match(r"Connected to radio", out) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info") | ||
| assert re.match(r"Connected to radio", out) | ||
| assert re.search(r"SECONDARY", out, re.MULTILINE) | ||
| assert re.search(r"testing1", out, re.MULTILINE) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-add testing2" | ||
| ) | ||
| assert re.match(r"Connected to radio", out) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info") | ||
| assert re.match(r"Connected to radio", out) | ||
| assert re.search(r"testing2", out, re.MULTILINE) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-del --ch-index 2" | ||
| ) | ||
| assert re.match(r"Connected to radio", out) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput("meshtastic --host localhost --info") | ||
| assert re.match(r"Connected to radio", out) | ||
| assert re.search(r"testing1", out, re.MULTILINE) | ||
| assert return_value == 0 | ||
| # pause for the radio | ||
| time.sleep(PAUSE_AFTER_COMMAND) | ||
| return_value, out = subprocess.getstatusoutput( | ||
| "meshtastic --host localhost --ch-del --ch-index 1" | ||
| time.sleep(2.0) | ||
|
|
||
| actual = iface.localNode.getURL() | ||
| assert "meshtastic.org" in actual, f"not a channel URL: {actual}" | ||
| # The firmware may reshape the URL and use slightly different base64 | ||
| # for the same data (trailing bits are ignored during decode), so we | ||
| # decode both and compare the protobuf payload prefix rather than | ||
| # doing a substring match on the encoded form. | ||
| _, _, frag = actual.partition("/#") | ||
| if not frag: | ||
| _, _, frag = actual.rpartition("#") | ||
| missing = len(frag) % 4 | ||
| if missing: | ||
| frag += "=" * (4 - missing) | ||
| actual_bytes: bytes = base64.urlsafe_b64decode(frag) | ||
|
|
||
| _, _, efrag = url.partition("/#") | ||
| missing = len(efrag) % 4 | ||
| if missing: | ||
| efrag += "=" * (4 - missing) | ||
| expected_bytes: bytes = base64.urlsafe_b64decode(efrag) | ||
|
|
||
| assert actual_bytes.startswith(expected_bytes), ( | ||
| f"URL payload mismatch:\n" | ||
| f" expected (hex): {expected_bytes.hex()}\n" | ||
| f" actual (hex): {actual_bytes.hex()}" | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
test_smokevirt_seturl_default never exercises the --seturl CLI flag.
The test's name/docstring claims to test --seturl, but it calls iface.localNode.setURL(url) directly on the fixture's Python interface — the CLI is never invoked. This means CLI argument parsing/handling for --seturl (which is explicitly tested via run_cli for the invalid-URL case right below) is not covered here, undermining the smoke-test's stated purpose of exercising the real CLI path against the simulator.
Suggested fix: invoke `run_cli` for the mutation, then verify via a fresh interface (consistent with the rest of the file)
`@pytest.mark.smokevirt`
def test_smokevirt_seturl_default(firmware_node):
"""--seturl applies a known channel URL."""
url = "https://www.meshtastic.org/d/#CgUYAyIBAQ"
- # Use the fixture's already-connected TCPInterface so the same
- # connection handles the firmware restart after setURL.
- if firmware_node.iface is None:
- pytest.fail("fixture interface not connected")
- iface = firmware_node.iface
- iface.localNode.setURL(url)
-
- time.sleep(2.0)
-
- actual = iface.localNode.getURL()
+ rc, out = run_cli(firmware_node.port, "--seturl", url)
+ assert rc == 0, out
+
+ def check(iface):
+ actual = iface.localNode.getURL()
+ ...
+ verify_state(firmware_node.port, check)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@meshtastic/tests/test_smokevirt.py` around lines 394 - 432,
`test_smokevirt_seturl_default` currently bypasses the `--seturl` CLI path by
calling `iface.localNode.setURL` directly, so it does not test argument parsing
or CLI behavior. Update the test to use `run_cli` for the URL mutation, then
reconnect or use a fresh interface to verify the resulting URL via
`firmware_node`/`iface.localNode.getURL`, keeping the existing payload-decoding
assertions intact.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@meshtastic/__main__.py`:
- Around line 972-980: Fix the incorrect protobuf accessor in the channel preset
handling inside __main__.py: the LONG_MODERATE and LONG_TURBO branches are using
config_pb2.config instead of the valid config_pb2.Config used by the neighboring
preset cases. Update the setSimpleConfig calls in that block so they all
reference config_pb2.Config.LoRaConfig.ModemPreset consistently, matching the
smoke test expectations and avoiding AttributeError for the affected CLI flags.
- Line 1972: The help text for the modem preset option contains a typo in the
deprecation message. Update the string in the argument definition for the
VERY_LONG_SLOW preset so it says “Deprecated since 2.5 firmware.” instead of
“Deprecated sincce 2.5 firmware.”
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bf9ca07f-07f4-45d3-99fc-6bae0c62bbc5
📒 Files selected for processing (2)
meshtastic/__main__.pymeshtastic/tests/test_smokevirt.py
🚧 Files skipped from review as they are similar to previous changes (1)
- meshtastic/tests/test_smokevirt.py
There was a problem hiding this comment.
🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)
74-106: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a job-level timeout to bound the whole
simradio_testingjob.Only the final pytest step has
timeout-minutes: 15; the PPA add/update/install and poetry install steps are unbounded. A hungapt-get update/PPA fetch (network flakiness against a third-party PPA) could tie up a runner well past the intended time budget.⏱️ Proposed fix
simradio_testing: runs-on: ubuntu-latest + timeout-minutes: 25 strategy: fail-fast: false🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 74 - 106, Add a job-level timeout to the simradio_testing workflow job so the entire matrix run is bounded, not just the final pytest step. Update the simradio_testing job definition in the CI workflow to enforce an overall timeout that covers the checkout, setup-python, poetry install, and meshtasticd PPA install steps, while keeping the existing Run firmware smoke tests step timeout as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 74-106: Add a job-level timeout to the simradio_testing workflow
job so the entire matrix run is bounded, not just the final pytest step. Update
the simradio_testing job definition in the CI workflow to enforce an overall
timeout that covers the checkout, setup-python, poetry install, and meshtasticd
PPA install steps, while keeping the existing Run firmware smoke tests step
timeout as-is.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1cf0571f-d7b0-4d62-8d64-691f55323075
📒 Files selected for processing (3)
.github/workflows/ci.ymlmeshtastic/__main__.pymeshtastic/tests/test_node.py
🚧 Files skipped from review as they are similar to previous changes (1)
- meshtastic/tests/test_node.py
When merged this should fix #643 as well. Still adding a bit of stuff to make
smokevirtnot so useless.Summary by CodeRabbit